package com.aliang.shopping.util;

import cn.hutool.core.collection.CollectionUtil;
import com.aliang.shopping.model.dto.RelateDTO;

import java.math.BigDecimal;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

/**
 * 用户推荐工具类
 */
public class UserCFUtil {



        /**
         * 方法描述: 推荐商品id列表
         *
         * @param userId 当前用户
         * @param list   用户商品评分数据
         * @return {@link List <Integer>}
         */
        public static List<Long> recommend(Long userId, List<RelateDTO> list) {
            // 按用户分组
            Map<Long, List<RelateDTO>> userMap = list.stream().collect(Collectors.groupingBy(RelateDTO::getUserId));
            // 获取其他用户与当前用户的关系值
            Map<Long, Double> userDisMap =  computeNeighbor(userId, userMap, 0);
            if (CollectionUtil.isEmpty(userDisMap)) {
                return Collections.emptyList();
            }
            // 获取关系最近的用户
            double maxValue = Collections.max(userDisMap.values());
            Set<Long> userIds = userDisMap.entrySet().stream().filter(e -> e.getValue() == maxValue).map(Map.Entry::getKey).collect(Collectors.toSet());
            // 取关系最近的用户
            Long nearestUserId = userIds.stream().findAny().orElse(null);
            if (nearestUserId == null) {
                return Collections.emptyList();
            }
            // 最近邻用户看过商品列表
            List<Long> neighborItems = userMap.get(nearestUserId).stream().map(RelateDTO::getGoodsId).collect(Collectors.toList());
            // 指定用户看过商品列表
            List<Long> userItems = userMap.get(userId).stream().map(RelateDTO::getGoodsId).collect(Collectors.toList());
            // 找到最近邻看过，但是该用户没看过的商品
            neighborItems.removeAll(userItems);
            return neighborItems;
    }


    /**
     * 计算相关系数并排序
     */
    public static Map<Long, Double> computeNeighbor(Long key, Map<Long, List<RelateDTO>> map, int type) {
        Map<Long, Double> distMap = new TreeMap<>();
        List<RelateDTO> userItems = map.get(key);
        if (CollectionUtil.isNotEmpty(userItems)) {
            map.forEach((k, v) -> {
                //排除此用户
                if (!k.equals(key)) {
                    //关系系数
                    double coefficient = relateDist(v, userItems, type);
                    //关系距离
                    double distance = Math.abs(coefficient);
                    distMap.put(k, distance);
                }
            });
        }
        return distMap;
    }


    /**
     * 计算两个序列间的相关系数
     */
    private static double relateDist(List<RelateDTO> xList, List<RelateDTO> yList, int type) {
        List<BigDecimal> xs = new ArrayList<>();
        List<BigDecimal> ys = new ArrayList<>();
        xList.forEach(x -> yList.forEach(y -> {
            if (type == 0) {
                if (x.getGoodsId().equals(y.getGoodsId())) {
                    xs.add(x.getIndex());
                    ys.add(y.getIndex());
                }
            } else {
                if (x.getUserId().equals(y.getUserId())) {
                    xs.add(x.getIndex());
                    ys.add(y.getIndex());
                }
            }
        }));
        return getRelate(xs, ys);
    }

    /**
     * 方法描述: 皮尔森（pearson）相关系数计算
     * @param xs x集合
     * @param ys y集合
     */
    public static double getRelate(List<BigDecimal> xs, List<BigDecimal> ys) {
        int n = xs.size();
        //至少有两个元素
        if (n < 2) {
            return 0D;
        }
        double Ex = xs.stream().reduce(BigDecimal::add).orElse(BigDecimal.ZERO).doubleValue();
        double Ey = ys.stream().reduce(BigDecimal::add).orElse(BigDecimal.ZERO).doubleValue();
        double Ex2 = xs.stream().map(x -> x.pow(2)).reduce(BigDecimal::add).orElse(BigDecimal.ZERO).doubleValue();
        double Ey2 = ys.stream().map(x -> x.pow(2)).reduce(BigDecimal::add).orElse(BigDecimal.ZERO).doubleValue();
        double Exy = IntStream.range(0, n).mapToDouble(i -> xs.get(i).multiply(ys.get(i)).doubleValue()).sum();
        double numerator = Exy - Ex * Ey / n;
        double denominator = Math.sqrt((Ex2 - Math.pow(Ex, 2) / n) * (Ey2 - Math.pow(Ey, 2) / n));
        if (denominator == 0) {
            return 0D;
        }
        return numerator / denominator;
    }
}
